]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
panel: Add workspaces indicators in activities button
authorGeorges Basile Stavracas Neto <georges.stavracas@gmail.com>
Thu, 10 Aug 2023 13:27:13 +0000 (10:27 -0300)
committerMarge Bot <marge-bot@gnome.org>
Sat, 19 Aug 2023 15:54:04 +0000 (15:54 +0000)
After removing the app name and icon, the next natural step that
was requested from the design team is to add workspaces indicators
to the top bar, where currently the Activities button is placed.

In addition to that, this is desired because there are known issues
with using "Activities" as a label for the overview. A more
comprehensive rationale for that can be found at [1].

Add an workspaces indicator replacing the Activities label in the
activities button.

The WorkspaceIndicators class controls how many workspaces dots
exists, their expansion, and the width multiplier. The WorkspaceDot
class takes the expansion and the multiplier, and applies it
internally so that we can get perfectly rounded dots at all
times without using CSS hacks.

The width multipliers are hardcoded, and defined by the design
team. We can revisit them later if necessary. Special care is
taken to not let these width multipliers result in fractional
widths.

When the number of workspaces changes, WorkspaceIndicators adds
new dot to the end, and animate them. When removing, scale the dot
out, then destroy it.

This does not work with workspace grids, but that's not supported
by GNOME Shell anyway, so no effort is made to cover this use case.

The button continues to have "Activities" as its accessible name,
but the label actor is removed.

Also adjust the padding of the activities pill, so it better wraps
the new indicators.

[1] https://gitlab.gnome.org/Teams/Design/os-mockups/-/issues/227

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2902>

data/theme/gnome-shell-sass/widgets/_panel.scss
js/ui/panel.js

index d36e3dc3cbf0b0d81c5bf6bc640028e4ab0fff8d..f23aa4b817a6e3ad86e8afa1243b0c425cdbdf16 100644 (file)
@@ -44,7 +44,18 @@ $panel_transition_duration: 250ms; // same as the overview transition duration
     }
 
     &#panelActivities {
-      -natural-hpadding: $base_padding * 3;
+      -natural-hpadding: $base_padding * 2.5;
+
+      & StBoxLayout {
+        spacing: 5px;
+      }
+
+      & .workspace-dot {
+        border-radius: 999px;
+        min-width: 8px;
+        min-height: 8px;
+        background-color: $panel_fg_color;
+      }
     }
 
     // screen activity indicators
@@ -90,6 +101,10 @@ $panel_transition_duration: 250ms; // same as the overview transition duration
     .panel-button {
       @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color);
 
+      &#panelActivities .workspace-dot {
+        background-color: $panel_system_fg_color;
+      }
+
       // clock
       &.clock-display {
         @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color, $highlighted_child: true, $child_class:".clock");
index 59007860835ce390c0dfec2cc553df4e91364239..fe16fab46e10d9ab54145894c5f3c0f155947ab4 100644 (file)
@@ -4,6 +4,7 @@ import Atk from 'gi://Atk';
 import Clutter from 'gi://Clutter';
 import GLib from 'gi://GLib';
 import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
 import Meta from 'gi://Meta';
 import Shell from 'gi://Shell';
 import St from 'gi://St';
@@ -18,6 +19,7 @@ import * as PopupMenu from './popupMenu.js';
 import * as PanelMenu from './panelMenu.js';
 import {QuickSettingsMenu, SystemIndicator} from './quickSettings.js';
 import * as Main from './main.js';
+import * as Util from '../misc/util.js';
 
 import * as RemoteAccessStatus from './status/remoteAccess.js';
 import * as PowerProfileStatus from './status/powerProfiles.js';
@@ -47,6 +49,8 @@ const BUTTON_DND_ACTIVATION_TIMEOUT = 250;
 
 const N_QUICK_SETTINGS_COLUMNS = 2;
 
+const INACTIVE_WORKSPACE_DOT_SCALE = 0.75;
+
 /**
  * AppMenuButton:
  *
@@ -256,23 +260,172 @@ const AppMenuButton = GObject.registerClass({
     }
 });
 
+const WorkspaceDot = GObject.registerClass({
+    Properties: {
+        'expansion': GObject.ParamSpec.double('expansion', '', '',
+            GObject.ParamFlags.READWRITE,
+            0.0, 1.0, 0.0),
+        'width-multiplier': GObject.ParamSpec.double(
+            'width-multiplier', '', '',
+            GObject.ParamFlags.READWRITE,
+            1.0, 10.0, 1.0),
+    },
+}, class WorkspaceDot extends Clutter.Actor {
+    constructor(params = {}) {
+        super({
+            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
+            ...params,
+        });
+
+        this._dot = new St.Widget({
+            style_class: 'workspace-dot',
+            y_align: Clutter.ActorAlign.CENTER,
+            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
+            request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT,
+        });
+        this.add_child(this._dot);
+
+        this.connect('notify::width-multiplier', () => this.queue_relayout());
+        this.connect('notify::expansion', () => {
+            this._updateVisuals();
+            this.queue_relayout();
+        });
+        this._updateVisuals();
+
+        this._destroying = false;
+    }
+
+    _updateVisuals() {
+        const {expansion} = this;
+
+        this._dot.set({
+            opacity: Util.lerp(0.50, 1.0, expansion) * 255,
+            scaleX: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion),
+            scaleY: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion),
+        });
+    }
+
+    vfunc_get_preferred_width(forHeight) {
+        const factor = Util.lerp(1.0, this.widthMultiplier, this.expansion);
+        return this._dot.get_preferred_width(forHeight).map(v => Math.round(v * factor));
+    }
+
+    vfunc_get_preferred_height(forWidth) {
+        return this._dot.get_preferred_height(forWidth);
+    }
+
+    vfunc_allocate(box) {
+        this.set_allocation(box);
+
+        box.set_origin(0, 0);
+        this._dot.allocate(box);
+    }
+
+    scaleIn() {
+        this.set({
+            scale_x: 0,
+            scale_y: 0,
+        });
+
+        this.ease({
+            duration: 500,
+            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
+            scale_x: 1.0,
+            scale_y: 1.0,
+        });
+    }
+
+    scaleOutAndDestroy() {
+        this._destroying = true;
+
+        this.ease({
+            duration: 500,
+            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
+            scale_x: 0.0,
+            scale_y: 0.0,
+            onComplete: () => this.destroy(),
+        });
+    }
+
+    get destroying() {
+        return this._destroying;
+    }
+});
+
+const WorkspaceIndicators = GObject.registerClass(
+class WorkspaceIndicators extends St.BoxLayout {
+    constructor() {
+        super();
+
+        this._workspacesAdjustment = Main.createWorkspacesAdjustment(this);
+        this._workspacesAdjustment.connectObject(
+            'notify::value', () => this._updateExpansion(),
+            'notify::upper', () => this._recalculateDots(),
+            this);
+
+        for (let i = 0; i < this._workspacesAdjustment.upper; i++)
+            this.insert_child_at_index(new WorkspaceDot(), i);
+        this._updateExpansion();
+    }
+
+    _getActiveIndicators() {
+        return [...this].filter(i => !i.destroying);
+    }
+
+    _recalculateDots() {
+        const activeIndicators = this._getActiveIndicators();
+        const nIndicators = activeIndicators.length;
+        const targetIndicators = this._workspacesAdjustment.upper;
+
+        let remaining = Math.abs(nIndicators - targetIndicators);
+        while (remaining--) {
+            if (nIndicators < targetIndicators) {
+                const indicator = new WorkspaceDot();
+                this.add_child(indicator);
+                indicator.scaleIn();
+            } else {
+                const indicator = activeIndicators[nIndicators - remaining - 1];
+                indicator.scaleOutAndDestroy();
+            }
+        }
+
+        this._updateExpansion();
+    }
+
+    _updateExpansion() {
+        const nIndicators = this._getActiveIndicators().length;
+        const activeWorkspace = this._workspacesAdjustment.value;
+
+        let widthMultiplier;
+        if (nIndicators <= 2)
+            widthMultiplier = 3.625;
+        else if (nIndicators <= 5)
+            widthMultiplier = 3.25;
+        else
+            widthMultiplier = 2.75;
+
+        this.get_children().forEach((indicator, index) => {
+            const distance = Math.abs(index - activeWorkspace);
+            indicator.expansion = Math.clamp(1 - distance, 0, 1);
+            indicator.widthMultiplier = widthMultiplier;
+        });
+    }
+});
+
 const ActivitiesButton = GObject.registerClass(
 class ActivitiesButton extends PanelMenu.Button {
     _init() {
         super._init(0.0, null, true);
-        this.accessible_role = Atk.Role.TOGGLE_BUTTON;
 
-        this.name = 'panelActivities';
-
-        /* Translators: If there is no suitable word for "Activities"
-           in your language, you can use the word for "Overview". */
-        this._label = new St.Label({
-            text: _('Activities'),
-            y_align: Clutter.ActorAlign.CENTER,
+        this.set({
+            name: 'panelActivities',
+            accessible_role: Atk.Role.TOGGLE_BUTTON,
+            /* Translators: If there is no suitable word for "Activities"
+               in your language, you can use the word for "Overview". */
+            accessible_name: _('Activities'),
         });
-        this.add_actor(this._label);
 
-        this.label_actor = this._label;
+        this.add_child(new WorkspaceIndicators());
 
         Main.overview.connect('showing', () => {
             this.add_style_pseudo_class('checked');